# -*- coding: utf-8 -*-

# NeoLight -- An application to turn your mobile device into a flashlight
#
# Copyright (C) 2009-2012 Valéry Febvre <vfebvre@easter-eggs.com>
# http://code.google.com/p/neolight/
#
# This file is part of NeoLight.
#
# NeoLight is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# NeoLight is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""
NeoLight -- An application to turn your mobile device into a flashlight
"""

import cPickle, os.path
import ecore, elementary

APP_VERSION = '1.4.1'
APP_NAME = 'NeoLight'
DATA_DIR = '/usr/share/neolight/'
#DATA_DIR = '../data'

STROBE_INTERVAL_MIN = 80
BRIGHTNESS_MIN = 10

DEFAULT_SETTINGS = {
    'mode':            False,
    'color':           '#FFFFFF',
    'custom_color':    '#4080C0',
    'brightness':      100,
    'strobe_mode':     'fcb',
    'strobe_interval': STROBE_INTERVAL_MIN,
    'fullscreen':      False,
    }

COLORS_LIST = (
    {'name': 'white', 'value': '#FFFFFF'},
    {'name': 'red',   'value': '#FF0000'},
    {'name': 'green', 'value': '#00FF00'},
    {'name': 'blue',  'value': '#0000FF'},
    )
COLORS = {}
map(lambda t: COLORS.update({t['value']: t}), COLORS_LIST)

# SOS : ... --- ...
# http://everything2.com/title/morse+code
# All elements in a character are separated by a period of silence equal in duration to a dot.
# One dash equals three dots.
# Characters are separated by a period of silence equal in duration to a dash.
# Words are separated by a period of silence equal in duration to two dashes. 

STROBE_MODES_LIST = (
    {
        'id':       'fcb',
        'name':     'Flashlight Color/Black',
        'interval': 'si',
        'colors':   ['fc', '#000000']
        },
    {
        'id':       'fcw',
        'name':     'Flashlight Color/White',
        'interval': 'si',
        'colors':   ['fc', '#FFFFFF']
        },
    {
        'id':       'sos',
        'name':     'SOS',
        'interval': 250,
        'colors':   [
            '#FFFFFF', '#000000', '#FFFFFF', '#000000', '#FFFFFF', '#000000', None, None,
            '#FFFFFF', None, None, '#000000', '#FFFFFF', None, None, '#000000', '#FFFFFF', None, None, '#000000', None, None,
            '#FFFFFF', '#000000', '#FFFFFF', '#000000', '#FFFFFF', '#000000', None, None, None, None, None
            ]
        },
    )
STROBE_MODES = {}
map(lambda t: STROBE_MODES.update({t['id']: t}), STROBE_MODES_LIST)


def color_hex_triplet_to_rgb(color):
    return (int('0x%s' % color[1:3], 16), int('0x%s' % color[3:5], 16), int('0x%s' % color[5:7], 16))

def color_rgb_to_hex_triplet(r, g, b):
    ht = hex(r * 65536 + g * 256 + b)[2:].upper()
    return '#' + (6 - len(ht)) * '0' + ht

class Icon(elementary.Icon):
    def __init__(self, parent, image):
        elementary.Icon.__init__(self, parent)
        self.file_set(os.path.join(DATA_DIR, 'images', image))


#
# Config
#

class Config():
    def __init__(self):
        cfg_dir = os.path.join(os.path.expanduser("~"), ".config/%s" % APP_NAME.lower())
        if not os.path.exists(cfg_dir):
            os.makedirs(cfg_dir, 0755)
        
        self.cfg_path = os.path.join(cfg_dir, '%s.conf' % APP_NAME.lower())
        if not os.path.exists(self.cfg_path):
            self.data = DEFAULT_SETTINGS
        else:
            self.data = cPickle.load(open(self.cfg_path, 'r'))

        self.is_dirty = False

    def save(self):
        if self.is_dirty:
            # NEVER save mode: always start in Ligth mode (strobe off)
            self.data['mode'] = False
            cPickle.dump(self.data, open(self.cfg_path, 'w+'))
            self.is_dirty = False

    def get(self, name):
        if name not in self.data.keys():
            # a new setting not available in user settings yet
            self.data[name] = DEFAULT_SETTINGS[name]
        return self.data[name]

    def set(self, name, value):
        if self.data[name] != value:
            self.data[name] = value
            self.is_dirty = True

cfg = Config()


#
# Light page
#

class LightPage(object):
    def __init__(self, main):
        self.main = main
        self.strobe_colors = None
        self.strobe_timer = None
        self.strobe_step = 0

        self.layout = elementary.Layout(self.main.win)
        self.layout.file_set(os.path.join(DATA_DIR, 'neolight.edj'), 'flashlight')
        self.layout.show()

        self.bg = self.layout.edje_get()
        self.bg.on_mouse_down_add(self.on_click)

        self.main.pager.item_simple_push(self.layout)

    def start_strobe(self):
        self.strobe_step = 0
        self.strobe_colors = STROBE_MODES[cfg.get('strobe_mode')]['colors'][:]
        for i, color in enumerate(self.strobe_colors):
            if color is None:
                continue
            if color == 'fc':
                color = cfg.get('color')
            self.strobe_colors[i] = color_hex_triplet_to_rgb(color) + (255,)
        # start timer
        interval = STROBE_MODES[cfg.get('strobe_mode')]['interval']
        if interval == 'si':
            interval = cfg.get('strobe_interval')
        self.strobe_timer = ecore.timer_add(interval / float(1000), self.play_strobe)

    def stop_strobe(self):
        if self.strobe_timer:
            self.strobe_timer.delete()
            self.strobe_timer = None

    def play_strobe(self):
        color = self.strobe_colors[self.strobe_step]
        if color is not None:
            self.bg.color = color
        self.strobe_step = (self.strobe_step + 1) % len(self.strobe_colors)
        return True

    def promote(self):
        if cfg.get('mode') == 1:
            self.start_strobe()
        else:
            self.bg.color = color_hex_triplet_to_rgb(cfg.get('color')) + (255,)
        self.main.pager.item_simple_promote(self.layout)

    def unpromote(self):
        self.stop_strobe()

    def on_click(self, bg, event):
        pos = event.position.canvas
        if pos.x <= 80 and self.main.win.size[1] - pos.y <= 80:
            self.main.show_settings_page()
        elif self.main.win.size[0] - pos.x <= 80 and self.main.win.size[1] - pos.y <= 80:
            self.main.quit()
        elif pos.x <= 80 and pos.y <= 80:
            self.main.settings_page.set_brightness(BRIGHTNESS_MIN)
        elif self.main.win.size[0] - pos.x <= 80 and pos.y <= 80:
            self.main.settings_page.set_brightness(cfg.get('brightness'))


#
# Settings page
#

class SettingsPage(object):
    def __init__(self, main):
        self.main = main
        self.box = None

    def build(self):
        self.box = elementary.Box(self.main.win)
        self.box.size_hint_align_set(-1, -1)
        self.box.show()

        scroller = elementary.Scroller(self.main.win)
        scroller.bounce_set(False, False)
        scroller.size_hint_weight_set(1.0, 1.0)
        scroller.size_hint_align_set(-1.0, -1.0)
        self.box.pack_end(scroller)
        scroller.show()

        box = elementary.Box(self.main.win)
        box.size_hint_weight_set(1, 1)
        scroller.content_set(box)
        box.show()

        # mode
        self.tg_mode = elementary.Check(self.main.win)
        self.tg_mode.text_set("Mode")
        self.tg_mode.style_set("toggle")
        self.tg_mode.size_hint_align_set(-1, 0)
        self.tg_mode.state_set(cfg.get('mode'))
        self.tg_mode.part_text_set("on", "Strobe")
        self.tg_mode.part_text_set("off", "Light")
        self.tg_mode.callback_changed_add(self.toggle_mode)
        box.pack_end(self.tg_mode)
        self.tg_mode.show()

        # fullscreen
        self.tg_fullscreen = elementary.Check(self.main.win)
        self.tg_fullscreen.text_set("Fullscreen")
        self.tg_fullscreen.style_set("toggle")
        self.tg_fullscreen.size_hint_align_set(-1, 0)
        self.tg_fullscreen.state_set(cfg.get('fullscreen'))
        self.tg_fullscreen.part_text_set("on", "Yes")
        self.tg_fullscreen.part_text_set("off", "No")
        self.tg_fullscreen.callback_changed_add(self.toggle_fullscreen)
        box.pack_end(self.tg_fullscreen)
        self.tg_fullscreen.show()

        #
        # flashlight
        #
        frame_flashlight = elementary.Frame(self.main.win)
        frame_flashlight.text_set("Flashlight")
        frame_flashlight.size_hint_align_set(-1, -1)
        box.pack_end(frame_flashlight)
        frame_flashlight.show()

        box_flashlight = elementary.Box(self.main.win)
        frame_flashlight.content_set(box_flashlight)
        box_flashlight.show()

        # colors
        self.hs_color = elementary.Hoversel(self.main.win)
        self.hs_color.hover_parent_set(self.main.win)
        # FIXME: why scale_set here ?
        self.hs_color.scale_set(1)
        self.hs_color.size_hint_align_set(-1.0, 0.0)
        box_flashlight.pack_end(self.hs_color)
        self.hs_color.show()
        self.change_color()

        for color in COLORS_LIST:
            self.hs_color.item_add(
                color['name'].capitalize(),
                os.path.join(DATA_DIR, 'images', '%s.png' % color['name']),
                elementary.ELM_ICON_FILE,
                self.change_color, color
                )
        self.hs_color.item_add(
            'Custom Color', '', elementary.ELM_ICON_NONE,
            self.change_color, {'name': 'Custom Color', 'value': None}
            )

        # brightness/light intensity
        self.slider_brightness = elementary.Slider(self.main.win)
        self.slider_brightness.text_set('Intensity ')
        self.slider_brightness.size_hint_align_set(-1, 0)
        self.slider_brightness.unit_format_set("   %.0f %%")
        self.slider_brightness.indicator_format_set("%.0f %%")
        self.slider_brightness.min_max_set(BRIGHTNESS_MIN, 100)
        self.slider_brightness.value = cfg.get('brightness')
        self.slider_brightness.callback_delay_changed_add(self.change_brightness)
        box_flashlight.pack_end(self.slider_brightness)
        self.slider_brightness.show()

        # custom color
        frame_color_custom = elementary.Frame(self.main.win)
        frame_color_custom.text_set("Custom Color")
        frame_color_custom.size_hint_align_set(-1, -1)
        box_flashlight.pack_end(frame_color_custom)
        frame_color_custom.show()

        box_color_custom = elementary.Box(self.main.win)
        frame_color_custom.content_set(box_color_custom)
        box_color_custom.show()

        custom_color = color_hex_triplet_to_rgb(cfg.get('custom_color'))
        self.slider_color_red = elementary.Slider(self.main.win)
        self.slider_color_red.text_set('Red    ')
        self.slider_color_red.size_hint_align_set(-1, 0)
        self.slider_color_red.unit_format_set("   %.0f")
        self.slider_color_red.indicator_format_set("%.0f")
        self.slider_color_red.min_max_set(0, 255)
        self.slider_color_red.value = custom_color[0]
        self.slider_color_red.callback_delay_changed_add(self.change_custom_color)
        box_color_custom.pack_end(self.slider_color_red)
        self.slider_color_red.show()

        self.slider_color_green = elementary.Slider(self.main.win)
        self.slider_color_green.text_set('Green ')
        self.slider_color_green.size_hint_align_set(-1, 0)
        self.slider_color_green.unit_format_set("   %.0f")
        self.slider_color_green.indicator_format_set("%.0f")
        self.slider_color_green.min_max_set(0, 255)
        self.slider_color_green.value = custom_color[1]
        self.slider_color_green.callback_delay_changed_add(self.change_custom_color)
        box_color_custom.pack_end(self.slider_color_green)
        self.slider_color_green.show()

        self.slider_color_blue = elementary.Slider(self.main.win)
        self.slider_color_blue.text_set('Blue    ')
        self.slider_color_blue.size_hint_align_set(-1, 0)
        self.slider_color_blue.unit_format_set("   %.0f")
        self.slider_color_blue.indicator_format_set("%.0f")
        self.slider_color_blue.min_max_set(0, 255)
        self.slider_color_blue.value = custom_color[2]
        self.slider_color_blue.callback_delay_changed_add(self.change_custom_color)
        box_color_custom.pack_end(self.slider_color_blue)
        self.slider_color_blue.show()

        #
        # strobe
        #
        frame_strobe = elementary.Frame(self.main.win)
        frame_strobe.text_set("Strobe")
        frame_strobe.size_hint_align_set(-1, -1)
        box.pack_end(frame_strobe)
        frame_strobe.show()

        box_strobe = elementary.Box(self.main.win)
        frame_strobe.content_set(box_strobe)
        box_strobe.show()

        # strobe modes
        self.hs_strobe_modes = elementary.Hoversel(self.main.win)
        self.hs_strobe_modes.hover_parent_set(self.main.win)
        # FIXME: why scale_set here ?
        self.hs_strobe_modes.scale_set(1)
        self.hs_strobe_modes.size_hint_align_set(-1.0, 0.0)
        box_strobe.pack_end(self.hs_strobe_modes)
        self.hs_strobe_modes.show()
        self.change_strobe_mode()

        for mode in STROBE_MODES_LIST:
            self.hs_strobe_modes.item_add(
                mode['name'],
                "arrow_right", elementary.ELM_ICON_STANDARD,
                self.change_strobe_mode, mode['id']
                )

        # strobe interval
        self.slider_strobe_interval = elementary.Slider(self.main.win)
        self.slider_strobe_interval.text_set('Interval ')
        self.slider_strobe_interval.size_hint_align_set(-1, 0)
        self.slider_strobe_interval.unit_format_set("   %.0f ms")
        self.slider_strobe_interval.indicator_format_set("%.0f ms")
        self.slider_strobe_interval.min_max_set(STROBE_INTERVAL_MIN, 1000)
        self.slider_strobe_interval.value = cfg.get('strobe_interval')
        self.slider_strobe_interval.callback_delay_changed_add(self.change_strobe_interval)
        box_strobe.pack_end(self.slider_strobe_interval)
        self.slider_strobe_interval.show()

        #
        # buttons
        #
        box_btns = elementary.Box(self.main.win)
        box_btns.horizontal_set(True)
        box_btns.homogeneous_set(True)
        box_btns.size_hint_align_set(-1.0, 0)
        self.box.pack_end(box_btns)
        box_btns.show()

        btn_light = elementary.Button(self.main.win)
        btn_light.text_set('Light')
        btn_light.icon_set(Icon(self.main.win, 'light.png'))
        btn_light.size_hint_weight_set(1, 0)
        btn_light.size_hint_align_set(-1, 0)
        btn_light.callback_clicked_add(self.main.show_light_page)
        box_btns.pack_end(btn_light)
        btn_light.show()

        btn_about = elementary.Button(self.main.win)
        btn_about.text_set('About')
        btn_about.icon_set(Icon(self.main.win, 'about.png'))
        btn_about.size_hint_weight_set(1, 0)
        btn_about.size_hint_align_set(-1, 0)
        btn_about.callback_clicked_add(self.main.show_about_page)
        box_btns.pack_end(btn_about)
        btn_about.show()

        self.main.pager.item_simple_push(self.box)

    def set_brightness(self, value):
        if self.main.dbus_display and self.main.brightness_system:
            self.main.dbus_display.SetBrightness(value)

    def change_brightness(self, *args):
        brightness = int(round(self.slider_brightness.value))
        self.set_brightness(brightness)
        cfg.set('brightness', brightness)

    def change_color(self, hs = None, item = None, color = None):
        if not color:
            if cfg.get('color') == cfg.get('custom_color'):
                color = {'name': 'Custom Color', 'value': cfg.get('color')}
            else:
                color = COLORS[cfg.get('color')]
        else:
            if color['name'] == 'Custom Color':
                cfg.set('color', cfg.get('custom_color'))
            else:
                cfg.set('color', color['value'])

        if color['name'] == 'Custom Color':
            self.hs_color.icon_set(elementary.Icon(self.main.win))
        else:
            self.hs_color.icon_set(Icon(self.main.win, '%s.png' % color['name']))
        self.hs_color.text_set('Color (%s)' % color['name'].capitalize())

    def change_custom_color(self, *args):
        custom_color = color_rgb_to_hex_triplet(
            int(round(self.slider_color_red.value)),
            int(round(self.slider_color_green.value)),
            int(round(self.slider_color_blue.value))
            )
        if cfg.get('color') == cfg.get('custom_color'):
            cfg.set('color', custom_color)
        cfg.set('custom_color', custom_color)

    def change_strobe_interval(self, *args):
        cfg.set('strobe_interval', int(round(self.slider_strobe_interval.value)))

    def change_strobe_mode(self, hs = None, item = None, mode = None):
        if mode is None:
            mode = cfg.get('strobe_mode')
        else:
            cfg.set('strobe_mode', mode)
        self.hs_strobe_modes.text_set('Mode (%s)' % STROBE_MODES[mode]['name'])

    def promote(self):
        if not self.box:
            self.build()
        self.main.pager.item_simple_promote(self.box)

    def toggle_mode(self, *args):
        cfg.set('mode', self.tg_mode.state_get())

    def toggle_fullscreen(self, *args):
        fullscreen = self.tg_fullscreen.state_get()
        if fullscreen != cfg.get('fullscreen'):
            self.main.win.fullscreen_set(fullscreen)
            cfg.set('fullscreen', fullscreen)


#
# About page
#

class AboutPage(object):
    def __init__(self, main):
        self.main = main
        self.box = None

    def build(self):
        # main box
        self.box = elementary.Box(self.main.win)
        self.box.size_hint_align_set(-1, -1)
        self.box.show()

        label = elementary.Label(self.main.win)
        label.text_set('<b>%s %s</>' % (APP_NAME, APP_VERSION))
        label.size_hint_align_set(0.5, 0.5)
        self.box.pack_end(label)
        label.show()

        scroller = elementary.Scroller(self.main.win)
        scroller.bounce_set(False, True)
        scroller.size_hint_weight_set(1.0, 1.0)
        scroller.size_hint_align_set(-1.0, -1.0)
        self.box.pack_end(scroller)
        scroller.show()

        box = elementary.Box(self.main.win)
        box.size_hint_weight_set(1, 1)
        scroller.content_set(box)
        box.show()

        about  = """\
<b>NeoLight</b> turns your phone into a flashlight.

It flips the brightness up to full and displays a fullscreen bright white rectangle.

<b>Copyright</> © 2009-2012 Valéry Febvre

<b>Licensed</> under the GNU GPL v3

<b>Homepage</> http://code.google.com/p/neolight/

If you like this program send me an email to vfebvre@easter-eggs.com

========

<b>Features</>

* Bright white or colored (red/green/blue/custom) screen
* Brightness dimmer
* Strobe light modes (with adjustable speed)
* Just tap the top left corner of screen to dim brightness to minimum, and the top right corner to flip the brightness up to the level defined in settings

========

<b>WARNING : Seizures</>

Some people (about 1 in 4000) may have seizures or blackouts triggered by light flashes or patterns.

Even people who have no history of seizures or epilepsy may have an undiagnosed condition that can cause these "photosensitive epileptic seizures".

These seizures may have a variety of symptoms, including lightheadedness, altered vision, eye or face twitching, jerking or shaking of arms or legs, disorientation, confusion, or momentary loss of awareness. Seizures may also cause loss of consciousness or convulsions that can lead to injury from falling down or striking nearby objects.

<b>Anyone who has had a seizure, loss of awareness, or other symptom linked to an epileptic condition should consult a doctor before using the Flashlight strobe feature.</>\
"""

        entry = elementary.Entry(self.main.win)
        entry.editable_set(False)
        entry.line_wrap_set(True)
        entry.size_hint_align_set(-1, -1)
        entry.entry_set(about.replace('\n', '<br>'))
        box.pack_end(entry)
        entry.show()

        # buttons
        box_btns = elementary.Box(self.main.win)
        box_btns.horizontal_set(True)
        box_btns.homogeneous_set(True)
        box_btns.size_hint_align_set(-1.0, 0)
        self.box.pack_end(box_btns)
        box_btns.show()

        btn_light = elementary.Button(self.main.win)
        btn_light.text_set('Light')
        btn_light.icon_set(Icon(self.main.win, 'light.png'))
        btn_light.size_hint_weight_set(1, 0)
        btn_light.size_hint_align_set(-1, 0)
        btn_light.callback_clicked_add(self.main.show_light_page)
        box_btns.pack_end(btn_light)
        btn_light.show()

        btn_settings = elementary.Button(self.main.win)
        btn_settings.text_set('Settings')
        btn_settings.icon_set(Icon(self.main.win, 'settings.png'))
        btn_settings.size_hint_weight_set(1 , 0)
        btn_settings.size_hint_align_set(-1, 0)
        btn_settings.callback_clicked_add(self.main.show_settings_page)
        box_btns.pack_end(btn_settings)
        btn_settings.show()

        self.main.pager.item_simple_push(self.box)

    def promote(self):
        if not self.box:
            self.build()
        self.main.pager.item_simple_promote(self.box)


class Neolight(object):
    def __init__(self):
        self.dbus_display = None
        self.dbus_usage = None
        ecore.idler_add(self.init_dbus_idler)

        self.win = elementary.Window(APP_NAME, elementary.ELM_WIN_BASIC)
        self.win.title_set(APP_NAME)
        self.win.fullscreen_set(cfg.get('fullscreen'))
        self.win.callback_delete_request_add(self.quit)

        bg = elementary.Background(self.win)
        self.win.resize_object_add(bg)
        bg.size_hint_weight_set(1, 1)
        bg.show()

        self.pager = elementary.Naviframe(self.win)
        self.pager.size_hint_weight_set(1.0, 1.0)
        self.win.resize_object_add(self.pager)
        self.pager.show()

        self.light_page = LightPage(self)
        self.settings_page = SettingsPage(self)
        self.about_page = AboutPage(self)

        self.show_light_page()

        self.win.resize(480, 640)
        self.win.show()

    def init_dbus_idler(self):
        import dbus, e_dbus

        dbus_system = dbus.SystemBus(mainloop = e_dbus.DBusEcoreMainLoop())        
        try:
            self.dbus_display = dbus.Interface(
                dbus_system.get_object("org.freesmartphone.odeviced", "/org/freesmartphone/Device/Display/0"),
                dbus_interface = "org.freesmartphone.Device.Display"
                )
            self.dbus_usage = dbus.Interface(
                dbus_system.get_object("org.freesmartphone.ousaged", "/org/freesmartphone/Usage"),
                dbus_interface = "org.freesmartphone.Usage"
                )
        except Exception, e:
            print "dbus error: %s" % e
            self.brightness_system = None
        else:
            self.brightness_system = self.dbus_display.GetBrightness()
            self.settings_page.set_brightness(cfg.get('brightness'))
            self.request_display(True)

        return False

    def quit(self, *args):
        self.request_display(False)
        # restore system brightness
        self.settings_page.set_brightness(self.brightness_system)
        # save settings
        cfg.save()
        # exit
        elementary.exit()

    def request_display(self, display):
        if not self.dbus_usage or self.dbus_usage.GetResourceState('Display') == display:
            return

        if display:
            self.dbus_usage.RequestResource('Display')
        else:
            try:
                self.dbus_usage.ReleaseResource('Display')
            except Exception:
                print "Got dbus exception while releasing display resource"
        
    def show_light_page(self, *args):
        self.request_display(True)
        self.light_page.promote()

    def show_settings_page(self, *args):
        self.request_display(False)
        self.light_page.unpromote()
        self.settings_page.promote()

    def show_about_page(self, *args):
        self.about_page.promote()


def main():
    elementary.init()
    Neolight()
    elementary.run()
    elementary.shutdown()
    return 0

if __name__ == "__main__":
    exit(main())
